﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Collections.ObjectModel;
using System.Configuration;
using System.IO;
using PSE.Framework.Threading;
using PSE.Framework.Common;

namespace PSE.Framework.Process
{
    public class HierarchicalProcess : HierarchicalProcess<IProcessService>
    {
        protected override Uri GetServiceAddress()
        {
            return new Uri(string.Format("net.pipe://localhost/Process/{0}", this.ProcessId));
        }
    }

    public abstract class HierarchicalProcess<TProcessService> : IHierarchicalProcess
        where TProcessService : IProcessService
    {
        public HierarchicalProcess()
        {
            // Criando timer do processo pai
            _checkParentIsAliveTimer = new SyncTimer(this.GetCheckParentInterval());
            _checkParentIsAliveTimer.Elapsed += new EventHandler(CheckParent);
            _checkParentIsAliveTimer.ElapsedException += new EventHandler<ExceptionEventArgs>(HandleElapsedException);

            // Criando timer do processo filho
            _checkChildIsAliveTimer = new SyncTimer(this.GetCheckChildInterval());
            _checkChildIsAliveTimer.Elapsed += new EventHandler(CheckChild);
            _checkChildIsAliveTimer.ElapsedException += new EventHandler<ExceptionEventArgs>(HandleElapsedException);
        }

        private bool _disposed = false;
        private System.Diagnostics.Process _process;
        private CrossProcessManualResetEvent _childProcessStarted = new CrossProcessManualResetEvent(false);

        private SyncTimer _checkParentIsAliveTimer;
        private SyncTimer _checkChildIsAliveTimer;

        private string _password = null;

        #region IsStarted
        /// <summary>
        /// Indica se o processo foi iniciado
        /// </summary>
        public bool IsStarted
        {
            get
            {
                bool started = false;

                if (_process != null)
                {
                    _process.Refresh();
                    started = !_process.HasExited;
                }

                return started;
            }
        }
        #endregion

        #region Name
        private string _name = null;

        public string Name
        {
            get { return _name; }
        }
        #endregion

        #region ProcessId
        public int ProcessId
        {
            get
            {
                if (this.IsStarted)
                    return _process.Id;
                else
                    return -1;
            }
        }
        #endregion

        #region ProcessType
        private HierarchicalProcessType? _processType = null;

        public HierarchicalProcessType? ProcessType
        {
            get { return _processType; }
        }
        #endregion

        #region FileName
        private string _fileName = null;

        public string FileName
        {
            get { return _fileName; }
        }
        #endregion

        #region Arguments
        private string _arguments = null;

        public string Arguments
        {
            get { return _arguments; }
        }
        #endregion

        #region Domain
        private string _domain = null;

        public string Domain
        {
            get { return _domain; }
        }
        #endregion

        #region UserName
        private string _userName = null;

        public string UserName
        {
            get { return _userName; }
        }
        #endregion

        #region CheckParentIsAlive
        private bool? _checkParentIsAlive = null;

        public bool? CheckParentIsAlive
        {
            get { return _checkParentIsAlive; }
        }
        #endregion

        #region CheckChildIsAlive
        private bool? _checkChildIsAlive = null;

        public bool? CheckChildIsAlive
        {
            get { return _checkChildIsAlive; }
        }
        #endregion

        #region ServiceAddress
        public Uri ServiceAddress
        {
            get { return this.GetServiceAddress(); }
        }
        #endregion

        #region ProcessManager
        private IHierarchicalProcessManager _processManager = null;

        public IHierarchicalProcessManager ProcessManager
        {
            get { return _processManager; }
        }
        #endregion

        #region Initialize
        public void Initialize(HierarchicalProcessStartInfo startInfo, IHierarchicalProcessManager processManager)
        {
            this.CheckIsDisposed();

            if (startInfo == null)
                throw new ArgumentNullException("startInfo");
            if (processManager == null)
                throw new ArgumentNullException("processManager");
            if (string.IsNullOrEmpty(startInfo.Name))
                throw new ArgumentNullException("startInfo.Name");
            if (string.IsNullOrEmpty(startInfo.FileName))
                throw new ArgumentNullException("startInfo.FileName");
            if (startInfo is ChildProcessStartInfo && this.IsStarted)
                throw new InvalidOperationException("Cannot start process, it is already started");

            try
            {
                _name = startInfo.Name;
                _fileName = startInfo.FileName;
                _arguments = startInfo.Arguments;
                _domain = startInfo.Domain;
                _userName = startInfo.UserName;
                _checkParentIsAlive = startInfo.CheckParentIsAlive;
                _checkChildIsAlive = startInfo.CheckChildIsAlive;
                _processManager = processManager;

                if (startInfo is ParentProcessStartInfo)
                {
                    _processType = HierarchicalProcessType.Parent;

                    _process = System.Diagnostics.Process.GetProcessById(((ParentProcessStartInfo)startInfo).ProcessId);

                    if (_process == null)
                        throw new InvalidOperationException(string.Format("Parent process PID '{0}' not found.", ((ParentProcessStartInfo)startInfo).ProcessId));

                    if (_process.ProcessName != Path.GetFileNameWithoutExtension(startInfo.FileName))
                        throw new InvalidOperationException(string.Format("Parent process PID '{0}' doesn't have the name '{1}'.", ((ParentProcessStartInfo)startInfo).ProcessId, startInfo.Name));

                    if ((bool)_checkParentIsAlive)
                        _checkParentIsAliveTimer.Start();
                }
                else if (startInfo is ChildProcessStartInfo)
                {
                    _processType = HierarchicalProcessType.Child;

                    _password = ((ChildProcessStartInfo)startInfo).Password;

                    this.StartProcess();

                    _checkChildIsAliveTimer.Start();
                }
                else
                {
                    throw new ArgumentException(string.Format("Invalid start info type '{0}'.", startInfo.GetType().AssemblyQualifiedName), "startInfo");
                }
            }
            catch
            {
                this.Dispose();
                throw;
            }
        }

        private bool _isParentReleased = false;

        public bool IsParentReleased
        {
            get { return _isParentReleased; }
        }

        public void ReleaseParent()
        {
            this.CheckIsDisposed();

            if (!this.IsParentReleased &&
                this.ProcessType == HierarchicalProcessType.Parent &&
                this.IsStarted)
            {
                using (ProcessServiceProxy<TProcessService> proxy = new ProcessServiceProxy<TProcessService>(this.GetServiceAddress()))
                {
                    proxy.GetInstance().ReleaseParent(this.ProcessManager.Name);
                }

                _isParentReleased = true;
            }
        }

        public void ReleaseChild()
        {
            this.CheckIsDisposed();

            _childProcessStarted.Set();
        }

        protected virtual TimeSpan GetStartChildTimeout()
        {
            return new TimeSpan(0, 1, 0);
        }

        protected abstract Uri GetServiceAddress();

        //#region Tratamento de eventos
        //private void ServiceHosterProcess_OutputDataReceived(object sender, DataReceivedEventArgs e)
        //{
        //    // Ao receber dados do processo filho libera o método Start para continuar
        //    _processStarted.Set();
        //}

        //private void ServiceHosterProcess_ErrorDataReceived(object sender, DataReceivedEventArgs e)
        //{
        //    _hasError = true;

        //    // Ao receber um erro do processo filho libera o método Start para continuar
        //    _processStarted.Set();
        //}
        //#endregion
        #endregion

        #region StartProcess
        /// <summary>
        /// Inicia o processo
        /// </summary>
        /// <returns>True se iniciou o processo com sucesso, False se ocorreu algum erro</returns>
        protected void StartProcess()
        {
            ProcessStartInfo startInfo = new ProcessStartInfo();

            startInfo.FileName = _fileName;
            startInfo.Arguments = this.GetCommandArguments();
            startInfo.CreateNoWindow = true;

            startInfo.UseShellExecute = false;

            // Indica que o output e os erros do processo serão direcionados para o pai
            //startInfo.RedirectStandardInput = true;
            //startInfo.RedirectStandardOutput = true;
            //startInfo.RedirectStandardError = true;


            if (string.IsNullOrEmpty(_userName) || string.IsNullOrEmpty(_domain))
            {
                //if (this.FileName == "HierarchicalProcessChild2.exe")
                //{
                //    EventLog.WriteEntry("HierarchicalProcessChild2 " + this.Name, string.Format("1 - '{0}\\{1}|{2}'", this.Domain, this.UserName, this._password), EventLogEntryType.Error);
                //}


                _process = new System.Diagnostics.Process();
                _process.StartInfo = startInfo;

                //_process.OutputDataReceived += new DataReceivedEventHandler(ServiceHosterProcess_OutputDataReceived);
                //_process.ErrorDataReceived += new DataReceivedEventHandler(ServiceHosterProcess_ErrorDataReceived);

                // Inicia o processo
                _process.Start();

                //_process.BeginErrorReadLine();
                //_process.BeginOutputReadLine();

                //// Espera o inicio do processo
                //_processStarted.WaitOne();

                //if (_hasError)
                //{
                //    throw new InvalidOperationException("Cannot start process, see log for details");
                //}

                //// Criando proxy do serviço do processo
                //using (ProcessServiceProxy<TProcessService> processService = new ProcessServiceProxy<TProcessService>(this.Address))
                //{
                //    processService.GetInstance().IsAlive();
                //}
            }
            else
            {
                // Utiliza API para iniciar o processo com outro usuário, System.Diagnostics.Process
                // dispara Access Denied

                _process = PSE.Framework.Process.RunAs.ProcessAsUser.Launch(_domain, _userName, _password, startInfo.FileName + " " + startInfo.Arguments);

                //if (this.FileName == "HierarchicalProcessChild2.exe")
                //{
                //    EventLog.WriteEntry("HierarchicalProcessChild2 " + this.Name, string.Format("2 - '{0}\\{1}|{2}'", this.Domain, this.UserName, this._password), EventLogEntryType.Error);
                //}


                //Exception lastEx = null;

                //// Espera 1 segundo pelo inicio do processo e tenta novamente por 5 vezes
                //for (int i = 0; i < 5; i++)
                //{
                //    Thread.Sleep(1000);

                //    try
                //    {
                //        // Criando proxy do serviço do processo
                //        using (ProcessService processService = new ProcessService(_packageName))
                //        {
                //            processService.GetInstance().IsAlive();
                //        }

                //        _hasError = false;
                //        break;
                //    }
                //    catch (Exception ex)
                //    {
                //        _hasError = true;
                //        lastEx = ex;
                //        continue;
                //    }
                //}

                //if (_hasError)
                //{
                //    throw new InvalidOperationException("Cannot start process, see log for details", lastEx);
                //}
            }

            // Habilita essa property para não ocorrer erro ao recuperar o ExitCode quando 
            // iniciar o processo pela API
            _process.EnableRaisingEvents = true;

            TimeSpan startChildTimeout = this.GetStartChildTimeout();

            // Aguarda o inicio do processo filho
            if (!_childProcessStarted.WaitOne(startChildTimeout))
            {
                throw new TimeoutException(string.Format("The child process '{0}' didn't start in the '{1}' timeout.", this.Name, startChildTimeout));
            }

            _childProcessStarted.Reset();
        }

        private string GetCommandArguments()
        {
            return string.Format("\"{0}\" \"{1}\" \"{2}\" \"{3}\" \"{4}\" \"{5}\" \"{6}\" \"{7}\"",
                this.Name,
                this.ProcessManager.ProcessId,
                this.ProcessManager.Name,
                this.ProcessManager.FileName,
                this.ProcessManager.Domain,
                this.ProcessManager.UserName,
                this.CheckParentIsAlive,
                this.CheckChildIsAlive) +
                (!string.IsNullOrEmpty(this.Arguments) ? " " + this.Arguments : null);
        }
        #endregion

        #region CheckParentIsAlive
        private void CheckParent(object sender, EventArgs e)
        {
            if (!this.IsStarted)
            {
                System.Diagnostics.Process.GetCurrentProcess().Kill();
            }
        }

        protected virtual TimeSpan GetCheckParentInterval()
        {
            return new TimeSpan(0, 0, 1);
        }
        #endregion

        #region CheckChildIsAlive
        private void CheckChild(object sender, EventArgs e)
        {
            if (!this.IsStarted)
            {
                // Verifica se o processo terminou com erro
                if ((bool)this.CheckChildIsAlive &&
                    _process != null &&
                    _process.ExitCode != 0)
                {
                    this.StartProcess();
                    this.OnRestarted(new EventArgs());
                }
                else
                {
                    this.Dispose();
                }
            }
        }

        protected virtual TimeSpan GetCheckChildInterval()
        {
            return new TimeSpan(0, 0, 1);
        }
        #endregion

        #region Restarted
        public event EventHandler Restarted;

        protected virtual void OnRestarted(EventArgs e)
        {
            if (Restarted != null)
                Restarted(this, e);
        }
        #endregion

        #region Closed
        public event EventHandler Closed;

        protected virtual void OnClosed(EventArgs e)
        {
            if (Closed != null)
                Closed(this, e);
        }
        #endregion

        #region CheckIsAliveException
        private void HandleElapsedException(object sender, ExceptionEventArgs e)
        {
            ProcessExceptionEventArgs args = new ProcessExceptionEventArgs(this, e.Exception);
            this.OnCheckIsAliveException(args);
            e.Handled = args.Handled;

            this.Dispose();
        }

        public event EventHandler<ProcessExceptionEventArgs> CheckIsAliveException;

        protected virtual void OnCheckIsAliveException(ProcessExceptionEventArgs e)
        {
            if (CheckIsAliveException != null)
                CheckIsAliveException(this, e);
        }
        #endregion

        #region IDisposable Members

        protected void CheckIsDisposed()
        {
            if (_disposed)
                throw new ObjectDisposedException(this.Name);
        }

        public void Dispose()
        {
            if (!_disposed)
            {
                // Finalizando timer do processo pai
                if (_checkParentIsAliveTimer != null)
                {
                    _checkParentIsAliveTimer.Dispose();
                    _checkParentIsAliveTimer.Elapsed -= new EventHandler(CheckParent);
                    _checkParentIsAliveTimer.ElapsedException -= new EventHandler<ExceptionEventArgs>(HandleElapsedException);
                    _checkParentIsAliveTimer = null;
                }

                // Finalizando timer do processo filho
                if (_checkChildIsAliveTimer != null)
                {
                    _checkChildIsAliveTimer.Dispose();
                    _checkChildIsAliveTimer.Elapsed -= new EventHandler(CheckChild);
                    _checkChildIsAliveTimer.ElapsedException -= new EventHandler<ExceptionEventArgs>(HandleElapsedException);
                    _checkChildIsAliveTimer = null;
                }

                // Libera o processo pai para continuar
                this.ReleaseParent();

                // Finalizando o processo
                if (this.IsStarted && this.ProcessType == HierarchicalProcessType.Child)
                {
                    try
                    {
                        using (ProcessServiceProxy<TProcessService> proxy = new ProcessServiceProxy<TProcessService>(this.GetServiceAddress()))
                        {
                            proxy.GetInstance().CloseChild();
                        }

                        _process.WaitForExit();
                    }
                    catch
                    {
                        try
                        {
                            if (this.IsStarted)
                                _process.Kill();
                        }
                        catch
                        {
                        }
                    }
                }

                if (_process != null)
                {
                    _process.Dispose();
                    _process = null;
                }

                _childProcessStarted.Set();
                _childProcessStarted.Reset();

                _disposed = true;

                this.OnClosed(new EventArgs());
            }
        }

        #endregion
    }
}
